/*
 * 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙，邂逅框架梦，本文采用木兰宽松许可证第2版]
 * 
 * https://zhiqim.org/project/zhiqim_kit/zhiqim_git.htm
 *
 * ZhiqimGit is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.zhiqim.git.refer;

import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.zhiqim.git.Git;
import org.zhiqim.git.GitConstants;
import org.zhiqim.git.objer.objectid.ObjectId;
import org.zhiqim.git.util.GitStreams;


/**
 * 引用组装器，用于HTTP更新包和接受包时交互
 *
 * @version v1.0.0 @author zouzhigang 2017-4-13 新建与整理
 */
public class RefAdvertiser implements GitConstants
{
    private final Git git;
    private final OutputStream out;
    
    private final StringBuilder strb = new StringBuilder(100);
    private final Set<String> capablities = new LinkedHashSet<>();
    private final Set<ObjectId> sent = new HashSet<ObjectId>();
    private boolean derefTags;                                              //是否剥除标签
    private boolean first = true;

    public RefAdvertiser(Git git, OutputStream out)
    {
        this.git = git;
        this.out = out;
    }
    
    public void sendAdvertisedRef(String service) throws IOException
    {
        this.writeOne("# service=" + service + "\n");
        this.end();
        
        if (UPLOAD_PACK.equals(service))
            this.sendUploadAdvertisedRef();
        else
            this.sendReceiveAdvertisedRef();
    }
    
    /** 发送更新包支持的功能和引用列表 */
    private void sendUploadAdvertisedRef() throws IOException
    {
        this.advertiseCapability(OPTION_INCLUDE_TAG);
        this.advertiseCapability(OPTION_MULTI_ACK_DETAILED);
        this.advertiseCapability(OPTION_MULTI_ACK);
        this.advertiseCapability(OPTION_OFS_DELTA);
        this.advertiseCapability(OPTION_SIDE_BAND);
        this.advertiseCapability(OPTION_SIDE_BAND_64K);
        this.advertiseCapability(OPTION_THIN_PACK);
        this.advertiseCapability(OPTION_NO_PROGRESS);
        
        Ref head = git.findRef(HEAD);
        if (head != null)
        {//增加头指向的目标分枝
            this.advertiseCapability(OPTION_SYMREF, HEAD + ':' + head.getTarget().getName());
        }
        
        this.setDerefTags(true);
        this.send(git.getRefMap());
        this.end();
    }
    
    /** 发送接受包支持的功能和引用列表 */
    private void sendReceiveAdvertisedRef() throws IOException
    {
        this.advertiseCapability(OPTION_SIDE_BAND_64K);
        this.advertiseCapability(OPTION_DELETE_REFS);
        this.advertiseCapability(OPTION_REPORT_STATUS);
        
        if (git.getConfig().getUseDeltaBaseOffset())
        {//支持打包增量
            this.advertiseCapability(OPTION_OFS_DELTA);
        }
        
        Map<String, Ref> refs = git.getRefMap();
        Ref head = refs.remove(HEAD);
        this.send(refs);
        
        if (head != null && !head.isSymbolic())
            this.advertiseHave(head.getObjectId());
        
        if (this.isEmpty())
            this.advertiseId(ZEROID, "capabilities^{}");
        
        this.end();
    }
    
    public void setDerefTags(final boolean deref)
    {
        derefTags = deref;
    }

    public void advertiseCapability(String name)
    {
        capablities.add(name);
    }

    public void advertiseCapability(String name, String value)
    {
        if (value != null)
        {
            capablities.add(name + '=' + value);
        }
    }
    
    private void send(final Map<String, Ref> refs) throws IOException
    {
        List<Ref> refList = RefComparator.sort(refs.values());
        for (Ref ref : refList)
        {
            ObjectId obj = ref.getObjectId();
            if (obj == null)
                continue;
            
            advertise(obj, ref.getName());
            
            if (!derefTags)
                continue;

            if (!ref.isPeeled())
            {
                ref = git.peel(ref);
            }

            if (ref.getPeeledObjectId() != null)
                advertise(ref.getPeeledObjectId(), ref.getName() + "^{}");
        }
    }

    private void advertiseHave(ObjectId id) throws IOException
    {
        advertiseOnce(id, ".have");
    }

    private boolean isEmpty()
    {
        return first;
    }

    private void advertiseOnce(ObjectId obj, String refName) throws IOException
    {
        if (!sent.contains(obj))
            advertise(obj, refName);
    }

    private void advertise(ObjectId obj, String refName) throws IOException
    {
        sent.add(obj);
        advertiseId(obj, refName);
    }

    private void advertiseId(ObjectId id, String refName) throws IOException
    {
        strb.setLength(0);
        strb.append(id.toHexCharArray()).append(_SPACE_).append(refName);
        
        if (first)
        {
            first = false;
            if (!capablities.isEmpty())
            {
                strb.append(_NUL_);
                for (String capablitie : capablities)
                {
                    strb.append(capablitie).append(_SPACE_);
                }
                strb.setLength(strb.length()-1);
            }
        }
        
        strb.append(_LF_);
        writeOne(strb);
    }

    private void writeOne(CharSequence line) throws IOException
    {
        GitStreams.writeString(out, line.toString());
    }

    private void end() throws IOException
    {
        GitStreams.writeEnd(out);
    }
}
